package be.rivendale.geometry;

import be.rivendale.mathematics.Point;
import be.rivendale.mathematics.Triangle;
import be.rivendale.mathematics.Vertex;
import org.apache.commons.lang.Validate;

import java.io.*;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

/**
 * Loads a Raw file containing text based triangle coordinates as a list of triangles.
 * <p>The ".raw" format is a format that's supported by <a href="http://www.blender.org/">Blender</a> as an export format.
 * Currently this loader only supports <strong>triangle data</strong>, so make sure you triangulate your meshes before
 * exporting them to the ".raw" file format.</p>
 * <p>The ".raw" file format is a very simple format that stores all triangle data in a text file.
 * The data layout is as follows:
 * <ul><li>There is one triangle per line</li>
 * <li>Each line contains exactly 9 decimal coordinates, each separated by a space</li>
 * <li>The 9 coordinates represent the vertices of each corner of the triangle (so an x, y and z coordinate for 3 points a, b, and c)</li>
 * <li>The coordinates are formatted according to the following java {@link java.text.DecimalFormat}: "0.000000"</li></ul>
 * An example of one line would be: <br/><code>1.066051 -0.615485 -0.095671 0.991692 -0.760952 0.000000 0.976594 -0.749367 -0.095671</code>
 * </p>
 */
public class RawFileLoader {
    /**
     * Loads a ".raw" file.
     * @param modelFile The file location of the model file to load.
     * @param relativePosition The relative position for the file to load.
     * The relative position is added to each coordinate in the file, so the model will actually be positioned relative to the specified point in the {@link be.rivendale.geometry.TriangleModel}.
     * @return The list of triangles loaded from the model file.
     */
    public static TriangleModel loadRawFile(String modelFile, Point relativePosition) {
        Validate.notNull(modelFile, "The modelFile is required");
        Validate.notNull(relativePosition, "The relative position is required");
        File file = new File(modelFile);
        Validate.isTrue(file.exists(), "The modelFile must exist");
        return new TriangleModel(openReaderLoadFileAndCloseBuffer(file, relativePosition));
    }

    public static TriangleModel loadRawFileByUrl(URL fileUrl, Point relativePosition) {
        Validate.notNull(fileUrl, "The modelFile is required");
        Validate.notNull(relativePosition, "The relative position is required");
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(fileUrl.openStream()));
            return new TriangleModel(readFile(reader, relativePosition));
        } catch (IOException exception) {
            throw new RuntimeException("Can't read file from url", exception);
        }
    }

    /**
	 * Creates a reader for the specified file, invokes the file parser to read the entire file
	 * and finally releases any IO resources obtained.
     * @param file The file to read.
     * @param relativePosition The relative position of all triangles.
     * @return The parsed list of triangles, relative to the specified position.
     */
	private static List<Triangle> openReaderLoadFileAndCloseBuffer(File file, Point relativePosition) {
		try {
			BufferedReader reader = null;
			try {
				reader = new BufferedReader(new FileReader(file));
				return readFile(reader, relativePosition);
			} finally {
				if(reader != null) {
					reader.close();
				}
			}
		} catch (IOException exception) {
			throw new IllegalArgumentException("Unable to load model file '" + file.getPath() + "'", exception);
		}
    }

    /**
	 * Reads the entire file from the specified reader. Handling of exceptions and management of IO resources
	 * is not required here: this is done at a higher level.
	 *
     *
     * @param reader The reader to read the entire file.
     * @param relativePosition The relative position of all triangles.
     * @throws java.io.IOException If any exception occurs while reading from the file.
     * @return The parsed list of triangles, relative to the specified position
	 */
	private static List<Triangle> readFile(BufferedReader reader, Point relativePosition) throws IOException {
        ArrayList<Triangle> triangles = new ArrayList<Triangle>();
        String faceLine = reader.readLine();
		while(faceLine != null) {
			triangles.add(parseFaceLine(faceLine, relativePosition));
			faceLine = reader.readLine();
		}
        return triangles;
	}

    /**
	 * Parses a line read from the file (a faceline) to a triangle.
	 * Each line contains information to build exactly one triangle.
	 *
     * @param faceLine The faceline that needs to be parsed and interpreted as a triangle.
     * @param relativePosition The relative position of each parsed triangle.
     * @return A triangle parsed from the faceline.
	 */
	private static Triangle parseFaceLine(String faceLine, Point relativePosition) {
		String[] coordinates = faceLine.split(" ");
		List<Vertex> vertices = parseVertices(coordinates, relativePosition);
		return new Triangle(vertices.get(2), vertices.get(1), vertices.get(0));

	}

    /**
	 * Validates that the specified coordinate array is valid to identify a triangle.
	 * If the coordinates array does not contain the expected amount of coordinates to identify a triangle,
	 * an {@link UnsupportedOperationException} is thrown.
	 *
     * @param coordinates An array of unparsed coordinates as found on a single line of the file.
     * @throws UnsupportedOperationException If the validation check fails.
	 * @return An array of the same length, containing the double parsed coordinates.
	 */
	private static double[] parseTriangleCoordinatesToDoubles(String[] coordinates) throws UnsupportedOperationException{
		double[] doubleCoordinates = new double[coordinates.length];
		for(int i = 0; i < coordinates.length; i++) {
			try {
				doubleCoordinates[i] = Double.parseDouble(coordinates[i]);
			} catch(NumberFormatException exception) {
				throw new IllegalArgumentException("The triangle coordinates can not be parsed to a double", exception);
			}
		}

		if(!areTriangleCoordinates(doubleCoordinates)) {
			throw new UnsupportedOperationException("The model contains non-triangle faces. This is not (yet) supported");
		}

		return doubleCoordinates;
	}

    /**
	 * Parses the array of coordinate strings into a list of {@link be.rivendale.mathematics.Vertex vertices}.
	 * The coordinate array must have a {@link #parseTriangleCoordinatesToDoubles(String[]) length of exactly 9} for triangles
	 * to be extracted from it. Since a single point consists of 3 coordinates, the returned list size will be exactly
	 * 3 <code>(9 / 3 = 3)</code>
	 * <p>If the size of the array is not as expected, an {@link UnsupportedOperationException} exception is thrown.</p>
	 *
     * @param coordinateStrings The individual coordinates as an unparsed string that should be interpreted as a list of 3
     * vertices.
     * @param relativePosition The relative position of each parsed vertex.
     * @return  A list of 3 vertices, containing XYZ coordinates as they occur in the coordinate array.
	 */
	private static List<Vertex> parseVertices(String[] coordinateStrings, Point relativePosition) {
		double[] doubleCoordinates = parseTriangleCoordinatesToDoubles(coordinateStrings);
		ArrayList<Vertex> vertices = new ArrayList<Vertex>();
        for(int vertexIndex = 0; vertexIndex < doubleCoordinates.length / 3; vertexIndex++) {
            double x = doubleCoordinates[(vertexIndex * 3) + 1] + relativePosition.getX();
            double y = doubleCoordinates[(vertexIndex * 3) + 0] + relativePosition.getY();
            double z = doubleCoordinates[(vertexIndex * 3) + 2] + relativePosition.getZ();
            vertices.add(new Vertex(x, y, z));
        }
        return vertices;
    }

    /**
	 * Checks if the specified coordiante array contains exactly the number of coordinates to identify a triangle.
	 * Since a triangle is defined by three points, each having an X, Y and Z coordinate in 3D space, the total number
	 * of coordinates should be <code>3 (points) x 3 (coodinates per point) = 9</code>
	 * @param coordinates The array of coordinates (as found on a single line of the file).
	 * @return True if the number of coordinates equals 9. False otherwise.
	 */
    private static boolean areTriangleCoordinates(double[] coordinates) {
        return coordinates.length == 3 * 3;
    }
}
